package ms.maplestory.boot.handlers.user;

import ms.maplestory.boot.Server;
import ms.maplestory.boot.client.Client;
import ms.maplestory.boot.client.Account;
import ms.maplestory.boot.client.User;
import ms.maplestory.boot.client.character.Char;
import ms.maplestory.boot.client.character.CharStat;
import ms.maplestory.boot.connection.InPacket;
import ms.maplestory.boot.connection.opcode.InHeader;
import ms.maplestory.boot.enums.login.*;
import ms.maplestory.boot.packets.LoginPacket;
import ms.maplestory.boot.constants.GameConstants;
import ms.maplestory.boot.enums.*;
import ms.maplestory.boot.handlers.Handler;
import ms.maplestory.boot.loaders.ItemData;
import ms.maplestory.boot.service.AccountService;
import ms.maplestory.boot.service.CharService;
import ms.maplestory.boot.service.CharStatService;
import ms.maplestory.boot.service.UserService;
import ms.maplestory.boot.service.helpers.CharHelper;
import ms.maplestory.boot.util.FileTime;
import ms.maplestory.boot.util.Util;
import ms.maplestory.boot.util.container.Tuple;
import ms.maplestory.boot.world.ChannelServer;
import ms.maplestory.boot.world.LoginServer;
import ms.maplestory.boot.world.WorldServer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ms.maplestory.boot.world.field.Field;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j(topic = "[LoginHandler]")
@Handler
@Component
@RequiredArgsConstructor
public class LoginHandler {

    private final Server server;
    private final AccountService accountService;
    private final UserService userService;
    private final CharService charService;
    private final CharStatService charStatService;

    /**
     * 请求验证登录密码
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CheckPassword)
    public void checkPasswordHandler(Client client, InPacket in) {
        String password = in.decodeString();
        String userName = in.decodeString();
        byte[] machineId = in.decodeArr(16);
        int gameRoomClient = in.decodeInt();
        byte gameStartMode = in.decodeByte();
        in.decodeArr(2); // byte 0 byte 0
        int partnerCode = in.decodeInt();
        LoginType loginType = LoginType.NotRegistered;
        Account account = accountService.getRepository().findByName(userName);
        if (account != null) {
            String dbPassword = account.getPassword();
            boolean hashed = Util.isStringBCrypt(dbPassword);
            if (hashed) {
                try {
                    loginType = BCrypt.checkpw(password, dbPassword) ? LoginType.Success : LoginType.IncorrectPassword;
                } catch (IllegalArgumentException e) {
                    log.error("bcrypt check in login has failed! dbPassword: {}; stack trace: {}", dbPassword, e.getStackTrace().toString());
                    loginType = LoginType.IncorrectPassword;
                }
            } else {
                loginType = password.equals(dbPassword) ? LoginType.Success : LoginType.IncorrectPassword;
            }
            if (LoginType.Success.equals(loginType)) {
                loginType = succeedLogin(client, account, machineId, hashed);
            }
        }
        log.info("userName:{},password:{},machineId:{},gameRoomClient:{},gameStartMode:{},partnerCode:{}",
                userName, password, machineId, gameRoomClient, gameStartMode, partnerCode);
        client.write(LoginPacket.sendCheckPassword(loginType, account));
    }

    /**
     * 登录帐号效验
     *
     * @param client
     * @param account        帐号信息类
     * @param machineId      机械码
     * @param passwordHashed 密码是否加密
     * @return
     */
    private LoginType succeedLogin(Client client, Account account, byte[] machineId, boolean passwordHashed) {
        LoginServer loginServer = LoginServer.getInstance();
        LoginType loginType = LoginType.Success;
        if (loginServer.isAccountLoggedIn(account)) {
            loginType = LoginType.AlreadyLoggedIn;
        } else if (account.getBlockExpireDate() != null && !account.getBlockExpireDate().isExpired()) {
            loginType = LoginType.Blocked;
        } else {
            if (!passwordHashed) {
                account.setPassword(BCrypt.hashpw(account.getPassword(), BCrypt.gensalt(10)));
                accountService.saveAndFlush(account);
            }
            loginServer.addAccount(account);
            client.setAccount(account);
            client.setMachineID(machineId);
        }
        return loginType;
    }

    /**
     * 请求角色列表
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_SelectWorld)
    public void selectWorldHandler(Client client, InPacket in) {
        byte gameStartMode = in.decodeByte();
        if (gameStartMode == 1) {
            String userName = in.decodeString();
            byte[] machineId = in.decodeArr(16);
            int gameRoomClient = in.decodeInt();
            gameStartMode = in.decodeByte();
        }
        int worldId = in.decodeByte();
        int channelId = in.decodeByte() + 1;
        byte[] address = in.decodeArr(4);
        Account account = client.getAccount();
        User user = userService.getAccountByWorldId(account, worldId);
        WorldServer worldServer = server.getWorldById(worldId);
        if (worldServer != null && worldServer.getChannelById(channelId) != null) {
            try {
                if (user == null) {
                    Account acc = this.accountService.initAccountUserWorld(account, worldId);
                    user = acc.getUserByWorldId(worldId);
                }
                client.setUser(user);
                client.setWorldId(worldId);
                client.setChannelId(channelId);
            } catch (Exception e) {
                log.error("UserWorld: {} Not Account, Init Error：{}", worldServer, e.getMessage());
                return;
            }
            client.write(LoginPacket.sendSelectWorld(user, false));
        }
    }

    /**
     * 请求世界服务器状态
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CheckUserLimit)
    public void checkUserLimitHandler(Client client, InPacket in) {
        short worldId = in.decodeShort();
        // TODO::检查世界服务器状态
        UserLimitType overUserLimit = UserLimitType.Normal;
        client.write(LoginPacket.sendCheckUserLimit(overUserLimit, 0));
    }

    /**
     * 请求验证PIN密码
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CheckPinCode)
    public void checkPinCodeHandler(Client client, InPacket in) {
        Account account = client.getAccount();
        byte type = in.decodeByte();
        if (type == 0) return;
        CheckPinCodeType checkPinCodeType = CheckPinCodeType.getByOpcode(in.decodeByte());
        String pinCode = in.decodeString();
        String dbPinCode = account.getPinCode();
        switch (type) {
            case 1:
            case 2:
                switch (checkPinCodeType) {
                    case Accepted:
                        try {
                            checkPinCodeType = BCrypt.checkpw(pinCode, dbPinCode)
                                    ? type == 2 ? CheckPinCodeType.Create : CheckPinCodeType.Accepted
                                    : CheckPinCodeType.Fail;
                        } catch (IllegalArgumentException e) {
                            log.error("bcrypt check in pinCode has failed! dbPinCode: {}; stack trace: {}", dbPinCode, e.getStackTrace().toString());
                            checkPinCodeType = CheckPinCodeType.Fail;
                        }
                        break;
                    case Create:
                        if (account.getPinCode() == null
                                || "".equals(account.getPinCode())) {
                            checkPinCodeType = CheckPinCodeType.Create;
                        } else {
                            checkPinCodeType = CheckPinCodeType.Request;
                        }
                        break;
                }
        }
        client.write(LoginPacket.sendCheckPinCode(checkPinCodeType));
    }

    /**
     * 请求修改PIN密码
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_UpdatePinCode)
    public void updatePinCodeHandler(Client client, InPacket in) {
        boolean success = in.decodeByte() == 1;
        Account account = client.getAccount();
        if (success) {
            String pinCode = in.decodeString();
            if (pinCode != null && pinCode.length() == 4) {
                account.setPinCode(BCrypt.hashpw(pinCode, BCrypt.gensalt(10)));
                accountService.saveAndFlush(account);
                client.write(LoginPacket.sendUpdatePinCode(false));
            }
        } else {
            client.write(LoginPacket.sendUpdatePinCode(true));
        }
    }

    /**
     * 请求世界服务器列表
     *
     * @param client
     * @param in
     */
    @Handler(ops = {InHeader.CP_WorldRequest, InHeader.CP_WorldInfoRequest})
    public void worldHandler(Client client, InPacket in) {
        server.getWorlds().forEach(worldServer -> {
            client.write(LoginPacket.sendWorldInformation(false, worldServer));
        });
        client.write(LoginPacket.sendWorldInformation(true, null));
        server.getWorlds().stream()
              .filter(worldServer -> client.getAccount() != null)
              .filter(worldServer -> worldServer.getId() == client.getAccount().getLatestConnectedWorld())
              .findFirst()
              .ifPresent(worldServer -> {
                  client.write(LoginPacket.sendLatestConnectedWorld(worldServer.getId()));
              });
        List<WorldServer> worldServers = server.getWorlds()
                                               .stream()
                                               .filter(WorldServer::isRecommend)
                                               .collect(Collectors.toList());
        client.write(LoginPacket.sendRecommendWorldMessage(worldServers));
    }

    /**
     * 请求退出世界服务器
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_LogoutWorld)
    public void LogoutWorldHandler(Client client, InPacket in) {

    }

    /**
     * 请求查看所有角色
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_ViewAllChar)
    public void viewAllCharHandler(Client client, InPacket in) {
        byte gameStartMode = in.decodeByte();
        Set<Char> chars = client.getAccount().getAllChar();
        if (chars.size() > 0) {
            client.write(LoginPacket.sendViewAllChar(ViewAllCharType.InitCount, null, chars.size(), ""));
        } else {
            client.write(LoginPacket.sendViewAllChar(ViewAllCharType.CountChangeStep, null, 0, ""));
        }
        for (User user : client.getAccount().getUsers()) {
            if (server.getWorldById(user.getWorldId()) != null) {
                client.write(LoginPacket.sendViewAllChar(ViewAllCharType.InitData, user, 0, ""));
            } else {
                client.write(LoginPacket.sendViewAllChar(ViewAllCharType.DataChangeStep, null, 0, ""));
            }
        }
    }

    /**
     * 请求进入游戏
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_MigrateIn)
    public void migrateInHandler(Client client, InPacket in) {
        int charId = in.decodeInt();
        byte[] machineId = in.decodeArr(16);
        boolean isUserGM = in.decodeByte() == 1;
        in.decodeArr(1); // byte 0
        in.decodeArr(8); // Buffer 8
        CharStat charStat = charStatService.getRepository().findByCharId(charId);
        if (charStat != null) {
            int worldId = charStat.getWorldId();
            Tuple<Integer, Client> info = server.getChannelFromTransfer(charId, worldId);
            int channelId = info.getLeft();
            Client oldClient = info.getRight();
            User user = oldClient.getUser();
            client.setUser(user);
            Account account = oldClient.getAccount();
            client.setAccount(account);
            server.getWorldById(worldId).getChannelById(channelId).removeClientFromTransfer(charId);
            client.setChannelId(channelId);
            client.setWorldId(worldId);
            client.setChannelServerInstance(server.getWorldById(worldId).getChannelById(channelId));
            Char chr = oldClient.getChr();
            if (chr == null || chr.getId() != charId) {
                chr = user.getCharById(charId);
            }
            account.setCurrentChr(chr);
            chr.setUser(user);
            chr.setClient(client);
            chr.setAccount(account);
            user.setAccount(account);
            client.setChr(chr);
            client.getChannelServerInstance().addChar(chr);
            LoginServer.getInstance().addAccount(account);
            Field field = CharHelper.getOrCreateFieldByCurrentInstanceType(chr, CharHelper.getFieldId(chr) <= 0 ? 100000000 : CharHelper.getFieldId(chr));
            CharHelper.warp(chr, field, true);
        } else {
            log.error("findByCharId Error.");
        }
    }

    /**
     * 请求检查角色名是否可用
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CheckDuplicatedID)
    public void checkDuplicatedIdHandler(Client client, InPacket in) {
        String name = in.decodeString();
        CheckDuplicatedIdType checkDuplicatedIdType;
        if (!GameConstants.isValidName(name)) {
            checkDuplicatedIdType = CheckDuplicatedIdType.No;
        } else {
            checkDuplicatedIdType = charService.getRepository().existsByNameAndWorld(name, client.getUser().getWorldId()) == null
                    ? CheckDuplicatedIdType.Available : CheckDuplicatedIdType.Occupied;
        }
        client.write(LoginPacket.sendCheckDuplicatedId(name, checkDuplicatedIdType));
    }

    /**
     * 创建角色请求
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CreateNewCharacter)
    public void createNewCharacterHandler(Client client, InPacket in) {
        User user = client.getUser();
        String name = in.decodeString();
        int curSelectedRace = in.decodeInt();
        short curSelectedSubJob = in.decodeShort();
        int[] items = new int[8];
        for (int i = 0; i < items.length; i++) {
            items[i] = in.decodeInt();
        }
        log.info("curSelectedRace:{},curSelectedSubJob:{},newCharItems:{}", curSelectedRace, curSelectedSubJob, items);
        byte gender = in.decodeByte();
        CheckDuplicatedIdType checkDuplicatedIdType = null;
        if (!ItemData.isStartingItems(items)) {
            user.getAccount().setBlockExpireDate(FileTime.currentTime());
            user.getAccount().setBlockReason("创建角色时出现初始化以外的装备。");
            userService.saveAndFlush(user);
            checkDuplicatedIdType = CheckDuplicatedIdType.Unknown;
        }
        if (!GameConstants.isValidName(name)) {
            checkDuplicatedIdType = CheckDuplicatedIdType.No;
        } else if (charService.getRepository().existsByNameAndWorld(name, user.getWorldId()) != null) {
            checkDuplicatedIdType = CheckDuplicatedIdType.Occupied;
        }
        if (checkDuplicatedIdType != null) {
            client.write(LoginPacket.sendCheckDuplicatedId(name, checkDuplicatedIdType));
            return;
        }
        CreateNewCharacterType createNewCharacterType = CreateNewCharacterType.Success;
        Char chr = charService.initChar(user, name, curSelectedRace, curSelectedSubJob, gender, items);
        if (chr == null) {
            createNewCharacterType = CreateNewCharacterType.Unknown;
        }
        client.write(LoginPacket.sendCreateNewCharacter(createNewCharacterType, chr));
    }

    /**
     * 选择角色请求-启用SPW
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_EnableSPWRequest)
    public void enableSPWHandler(Client client, InPacket in) {
        in.decodeArr(1); // byte 1
        int charId = in.decodeInt();
        String macAddress = in.decodeString();
        String macAddressWithHDD = in.decodeString();
        String spw = in.decodeString();
        User user = client.getUser();
        if (spw != null && spw.length() == 6) {
            if (BCrypt.checkpw(spw, client.getAccount().getPassword())) {
                client.write(LoginPacket.sendEnableSPW(false, EnableSPWOp.Different));
            } else {
                user.setSpw(BCrypt.hashpw(spw, BCrypt.gensalt(10)));
                userService.saveAndFlush(user);
                migrateIn(client, charId);
            }
        } else {
            client.write(LoginPacket.sendEnableSPW(false, EnableSPWOp.Unknown));
        }
    }

    /**
     * 选择角色请求-验证SPW
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_CheckSPWRequest)
    public void checkSPWHandler(Client client, InPacket in) {
        String spw = in.decodeString();
        int charId = in.decodeInt();
        String macAddress = in.decodeString();
        String macAddressWithHDD = in.decodeString();
        User user = client.getUser();
        if (spw != null && spw.length() == 6) {
            if (BCrypt.checkpw(spw, user.getSpw())) {
                migrateIn(client, charId);
            } else {
                client.write(LoginPacket.sendEnableSPW(false, EnableSPWOp.Incorrect));
            }
        } else {
            client.write(LoginPacket.sendEnableSPW(false, EnableSPWOp.Unknown));
        }
    }

    /**
     * 进入游戏
     *
     * @param client
     * @param charId
     */
    private void migrateIn(Client client, int charId) {
        User user = client.getUser();
        Account account = client.getAccount();
        account.setLatestConnectedWorld(user.getWorldId());
        accountService.saveAndFlush(account);
        int channelId = client.getChannelId();
        ChannelServer channelServer = server.getWorldById(client.getWorldId())
                                            .getChannelById(channelId);
        channelServer.addClientInTransfer(channelId, charId, client);
        client.write(LoginPacket.sendSelectCharacter(SelectCharacterType.Success, 11, channelServer, charId));
    }

    /**
     * 记录客户端错误日志请求
     *
     * @param client
     * @param in
     */
    @Handler(op = InHeader.CP_ClientDumpLog)
    public void clientDumpLogHandler(Client client, InPacket in) {
        short callType = in.decodeShort();
        int errorCode = in.decodeInt();
        short backupBufferSize = in.decodeShort(); // 12
        in.decodeArr(4); // 0
        int clearStackLog = in.decodeInt();
        in.decodeArr(4); // 0
        log.error("callType:{},errorCode:{},backupBufferSize:{},clearStackLog:{}\t丨{}",
                callType, errorCode, backupBufferSize, clearStackLog, in);
    }
}
